package peerToPeerFacebook.proj;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;

/** Base class for RPC infrastructure. This class provides common functionality shared between client and
 * server code.
 */
public abstract class RPCBase
{
	protected enum Protocol
	{
		RPC(0);
		
		public final int ID;
		
		private Protocol(int id)
		{
			this.ID = id;
		}
	}
	
	/** Node instance that this class is running on. */
	protected INode node = null;

	/** Constructor - creates node instance.
	 * 
	 * @param node Node that we are running on. This parameter will be used to
	 * 	send messages, create timeouts and persistent storage.
	 */
	public RPCBase(INode node)
	{
		ArgumentValidator.IsNotNull("node", node);		
		this.node = node;
	}

	/** Method that will be called when new messages has arrived.
	 * 
	 * @param senderID Server message is coming from
	 * @param protocol Protocol ID
	 * @param msg Payload of the message
	 */
	public void onReceive(int senderID, int protocol, byte[] msg)
	{
		// We do not care about not RPC messages
		if (protocol != Protocol.RPC.ID)
		{
			node.logSynopticEvent("Received non-rpc packet. From: " + senderID + ", protocol: " + protocol);
			return;
		}
		
		// send the message to implementation. It may or may not care. We also need to deal with exceptions here		
		try 
		{
			processPacket(senderID, RPCInvokePacket.decode(msg));
		} 
		catch (RPCException e) 
		{
			node.logSynopticEvent("Failed to decode RPC packet. Assume that it is grbage and dropping it: " +
					e.getMessage());
		}
	}
	
	/** Function that will be triggered when RPC packet is received
	 * 
	 * @param from Server that sent the packet
	 * @param packet packet sent.
	 */
	protected abstract void processPacket(int from, RPCPacketBase packet);
	
	/** Returns OutputStream that should be used for persistent data storage.
	 * 
	 * @param filename Name of the requested file
	 * @return Stream that should be used for storing data.
	 * @throws IOException
	 */
	protected OutputStream getOut(String filename) throws IOException
	{
		// We never want to append.
		return node.getOutputStream("rpc-" + filename, false);
	}
	
	/** Input stream that should be used for writing data.
	 * 
	 * @param filename Desired name of the file
	 * @return InputStream that should be used for reading or null if file cannot be created.
	 */
	protected InputStream getIn(String filename)
	{	
		try
		{
			return node.getInputStream("rpc-" + filename);
		} catch (FileNotFoundException e)
		{
			// If file is not there we should return null and caller should deal with that
			return null;			
		}
	}

	/** Writes object to persistent storage. Using this operation guarantees that data returned by
	 * readFromDisk will always be consistent. I.e. either read function sees the change or it will not.
	 * Even if write fails at some point read function will be able to extract either previous data or new one,
	 * depending where crash happened.
	 * 
	 * writeToDisk and readFromDisk will produce atomic read/write operations.
	 *
	 * @param fileName Name of the file
	 * @param object Object to write
	 * @throws IOException
	 */
	protected <T extends Serializable> void writeToDisk(String fileName, T object)
	{ 
		// Due to the way Simulator is implemented we write all data to disk. This cuts on the number of enters to use
		// as well as decrease our chances to corrupt storage.
		// The problem is that if write fails, RPC will be brought down as it assumes there is nothing that can be done.
		// I.e. usually if one write failed it is unreasonable to assume that that repeat of the same write would
		// succeed. To workaround that first we write data to the temp file, and only then we move it to real file.
		// If one of these calls crashes - we stil have one file that is valid
		
		OutputStream disk = null;
		OutputStream tempDisk = null;
		try {
			// Convert object to bytes
			ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
			ObjectOutputStream out = new ObjectOutputStream(byteOut);
			out.writeObject(object);
			out.close();
			
			// Flush first to temp file.
			// In case we fail here - we will have correct data stored in our original file
			tempDisk = getOut(generateTempFileName(fileName));
			tempDisk.write(byteOut.toByteArray());
			tempDisk.close();
			
			// Now we know that temp file contains valid data. So we can update original file.
			disk = getOut(fileName);
			disk.write(byteOut.toByteArray());
			disk.close();
			
			// At this point we should delete tempDisk, but as it is a bit difficult to do that and there is not side
			// effects we can leave it there.
		} catch(IOException ex)
		{
			// Ok we failed to store to the disk at some point. This failure is not node crash so this is prety much end
			// of the world for us. Fail.
			throw new RPCBroken("Failed to store to the disk", ex);
		}
	}

	/** Loads object from persistent storage
	 * 
	 * @param fileName Name of the file to read from
	 * @return Object read.
	 * @throws IOException
	 */
	protected <T extends Serializable> T readFromDisk(String fileName)
	{
		// First we try to read from the temp file. And if that fails then we revert back to read from the real filename.
		// At that point we will make sure that we get either information pre-write if we failed due to the crash
		// or post-write if everything worked fine.
		T object;
		try {
			object = tryReadFromDisk(generateTempFileName(fileName));
		} catch (IOException e) {
			// ok, temp file reading failed. Which is not good. Try original file
			try {
				object = tryReadFromDisk(fileName);
			} catch (IOException e1) {
				// Ok at this point our both files are corrupt. There is nothing else we can do - someone manually
				// corrupted both files. Bailing out
				throw new RPCBroken("Persisten storage is broken beyond repair", e1);
			}
		}
		
		return object;
	}

	/** Tries reading from disk. If that fails throws IOException> There could be a lot of reasons for failure, for
	 * example file missing, corrupted storage and so on.
	 * 
	 * @param fileName File name to read from.
	 * @return Object read
	 */
	@SuppressWarnings("unchecked")
	protected <T extends Serializable> T tryReadFromDisk(String fileName) throws IOException
	{
		InputStream in = getIn(fileName);
		ObjectInputStream dataIn = null;
		T object = null;
		
		// if file is there, let's read the data and see what happens
		if (in != null)
			try {
				dataIn = new ObjectInputStream(in);
				object = (T) dataIn.readObject();
			} catch (Exception ex) {
				// We failed to read. There could be a lot of reaqsons for that, e.g. file is corrupt. We just report
				// them back and move on
				throw new IOException("Persistent storage corrupt: " + fileName, ex);
			}
			finally {
				in.close();
				if (dataIn != null)
					dataIn.close();
			}
		
		return object;
	}
	
	/** Generates temp file name that should be used when buffering writtes
	 * 
	 * @param fileName Name of the file
	 * @return tempName of the file based on input name
	 */
	protected String generateTempFileName(String fileName)
	{
		return fileName + "-temp";
	}
}